Skip to content

a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background#3477

Merged
bilal-karim merged 1 commit into
mainfrom
a11y/1.4.11-checkbox-checked-focus-ring
Jun 3, 2026
Merged

a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background#3477
bilal-karim merged 1 commit into
mainfrom
a11y/1.4.11-checkbox-checked-focus-ring

Conversation

@canvanooo
Copy link
Copy Markdown
Contributor

@canvanooo canvanooo commented May 29, 2026

Description & motivation 💭

The Holocene Checkbox primitive renders its :checked state with an indigo background (peer-checked:bg-interactive--color-interactive-surface = indigo.600 #444CE7 in both modes per src/lib/theme/variables.ts:124-127) and applies a peer-focus-visible:ring-2 peer-focus-visible:ring-primary/70 focus ring. ring-primary maps to --color-border-focus-info = indigo.600 in both modes (src/lib/theme/plugin.ts:221, variables.ts:213-216).

When a checkbox is checked and focused, Tailwind composites a 2 px ring whose color is indigo.600 at 70% alpha directly on top of an indigo.600 background. Effective ring color equals background color, contrast ratio ~1.00 : 1, focus indicator invisible. Keyboard users tabbing through a form lose the focus indicator the moment a checkbox is checked.

The unchecked-state ring is unaffected (it composites over surface-primary, not over indigo). This PR fixes the checked-state-only failure by adding a 2 px ring-offset between the indigo background and the indigo ring, so the ring composites against the surrounding canvas (surface-primary) rather than against itself.

The diff:

  'peer-focus-visible:ring-2',
  'peer-focus-visible:ring-primary/70',
+ 'peer-focus-visible:ring-offset-2',
+ 'peer-focus-visible:ring-offset-[var(--color-surface-primary)]',

Two lines in src/lib/holocene/checkbox.svelte, inside the !disabled class array of the visual-surrogate <span>.

Design-system finding to surface to reviewers. The arbitrary-value form ring-offset-[var(--color-surface-primary)] is used here rather than the bare class ring-offset-surface-primary. The latter is a silent no-op in the current Tailwind config: src/lib/theme/plugin.ts extends theme.ringColor with primary/danger/success/brand but does NOT extend theme.ringOffsetColor, and the base theme.colors palette has no surface-primary key. I verified empirically with pnpm exec tailwindcss against both class strings — ring-offset-surface-primary generates zero CSS rules (falls back to default #fff); ring-offset-[var(--color-surface-primary)] generates --tw-ring-offset-color: var(--color-surface-primary) correctly. The previously-merged Button-primary focus-ring fix (#3438) uses the bare-class form — it works in light mode by coincidence (default #fff matches light-mode surface-primary) and is visible-but-mis-colored in dark mode. This PR uses the arbitrary-value form so dark mode is also correct. A separate follow-up may want to migrate Button to the same form; that's out of scope here.

Composes with PR #3478. PR #3478 (in flight) shifts the dark-mode value of --color-border-focus-info from indigo.600 to indigo.400 so ring-primary/70 composites at 3.78:1 against dark canvas. That fix addresses what color the ring is. This PR addresses where the ring sits (offset). Together they bring the dark-mode focused-and-checked checkbox to ~8.43:1 ring-vs-canvas contrast. They're independent and can land in either order.

Screenshots (if applicable) 📸

Screenshots to be captured by the PR author from the Vercel preview build (link appears once the Vercel check passes). Include light-mode and dark-mode captures for each affected primitive.

Design Considerations 🎨

The rendered checkbox now has slightly more visual weight when focused-and-checked (4 px total of ring + gap vs the previous 2 px ring overlapping the block). For unchecked + focused, no visual change. Design team may want to confirm the focused-and-checked rendering is acceptable; it's necessary to meet WCAG 1.4.11 because the underlying indigo-on-indigo overlap is what makes the ring invisible.

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Automated checks performed locally on a11y/1.4.11-checkbox-checked-focus-ring before pushing:

  • pnpm lint — 0 errors
  • pnpm check (svelte-check) — 0 errors (84 pre-existing warnings repo-wide, none in checkbox.svelte)
  • pnpm test -- --run — 142 test files / 2023 tests pass
  • Pre-commit lint hooks (lint-staged: eslint --fix, prettier --write, stylelint --fix) clean on the modified file
  • Tailwind class-generation probe (pnpm exec tailwindcss) confirms ring-offset-[var(--color-surface-primary)] emits real CSS (--tw-ring-offset-color: var(--color-surface-primary))

Manual visual testing in Storybook is the responsibility of the PR author after the preview deploy is ready (see "Steps for others to test" below).

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Check out the branch and pnpm install if needed.
  2. pnpm stories:dev — open Storybook at http://localhost:6006.
  3. Sidebar → Holocene → Checkbox → Default story.
  4. Click the checkbox to toggle it checked. Click outside, then Tab back onto it.
  5. Confirm: visible ring around the checked checkbox, separated from the indigo block by a small gap (indigo block → white gap → indigo ring → page surface).
  6. DevTools eyedropper: ring color materially different from the indigo block color; gap visible.
  7. Toggle Storybook to dark mode (theme toolbar). Repeat steps 4-6. Confirm: gap is black (not white), ring visible. Note: in dark mode the ring contrast still depends on PR a11y(1.4.11): focus rings — lighten dark-mode --color-border-focus-info to indigo.400 so ring-primary/70 meets 3:1 against surface-primary #3478 landing too — see the "Composes with PR a11y(1.4.11): focus rings — lighten dark-mode --color-border-focus-info to indigo.400 so ring-primary/70 meets 3:1 against surface-primary #3478" note above.
  8. Hover (don't focus) over the checkbox. Confirm: hover ring renders unchanged (the new offset only applies to focus-visible).
  9. Try a disabled checkbox. Confirm: no focus ring fires (the new lines are inside the !disabled array).
  10. Try an unchecked focused checkbox. Confirm: rendering identical to current main — the offset is a visual no-op here because the checkbox background already equals surface-primary.
  11. Cross-browser: confirm in Chromium and Firefox.

Checklists

Draft Checklist

  • Two-line diff verified in src/lib/holocene/checkbox.svelte (inside the !disabled class array)
  • Tailwind class generation confirmed via pnpm exec tailwindcss probe
  • Design-system finding (no-op ring-offset-surface-primary in current config) surfaced in PR description
  • Local pnpm lint, pnpm check, pnpm test -- --run all pass

Merge Checklist

  • PR author has walked the Storybook sweep above
  • Light-mode and dark-mode focused-checked rendering both verified
  • Cross-browser parity (Chromium + Firefox)
  • CLA status green
  • Design sign-off on the rendered focused-checked appearance (visible gap between indigo block and indigo ring)

Issue(s) closed

A11y-Audit-Ref: 1.4.11-checkbox-checked-focus-ring

Closes the checkbox checked-state focus-ring defect documented in the May 2026 audit (manifest bucket 1, severity serious, scope ui-main). See scripts/a11y/manifest.yml for the canonical entry.

Docs

Any docs updates needed?

No external docs (docs.temporal.io) need updating — this is a consumer-side class addition with no API surface change.

🤖 Generated with Claude Code

…ing contrasts against the indigo checked background

The Holocene Checkbox primitive's :checked state applied
peer-checked:bg-interactive (--color-interactive-surface = indigo.600 in
both modes) with a peer-focus-visible:ring-primary/70 focus ring, where
ring-primary maps to --color-border-focus-info (also indigo.600 in both
modes). The composited ring color equalled the checked background color
(ratio 1.00:1), so the focus indicator was invisible whenever a checkbox
was both checked and focused — the most common state for tab-traversing a
form.

This adds peer-focus-visible:ring-offset-2 + ring-offset-[var(--color-
surface-primary)] on the visual surrogate. The 2 px gap sits between the
indigo block and the indigo ring, and the gap is colored to match the
surrounding page surface (white in light mode, black in dark mode) so the
ring composites against canvas rather than against itself. Post-fix ratio
against the gap is ~3.58:1 in light mode (Pass).

Note: the existing Button-primary fix in #3438 used the class
ring-offset-surface-primary, which is a silent no-op in the current
Tailwind config (no surface-primary key in theme.ringOffsetColor or
theme.colors). It worked there only because Tailwind's default
--tw-ring-offset-color (#fff) happens to match light-mode surface-primary
and is visible-but-mis-colored in dark mode. The arbitrary-value form
used here, ring-offset-[var(--color-surface-primary)], passes the design
token through verbatim and generates a real CSS rule that is correct in
both modes. A separate follow-up may want to apply the same arbitrary-
value treatment to Button.

Cross-walks 2.4.7 Focus Visible (Level AA). Cascades to cloud-ui-main via
the @temporalio/ui tarball on next repack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment May 29, 2026 4:13pm

Request Review

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-1 Bucket 1: design-mergeable, CSS / tokens a11y:sc-1.4.11 labels May 29, 2026
@temporal-cicd
Copy link
Copy Markdown
Contributor

temporal-cicd Bot commented May 29, 2026

Warnings
⚠️

📊 Strict Mode: 3 errors in 1 file (0.3% of 898 total)

src/lib/holocene/checkbox.svelte (3)
  • L33:13: Type 'undefined' is not assignable to type 'T'.
  • L34:13: Type 'undefined' is not assignable to type 'T[]'.
  • L13:12: Argument of type '$$Props' is not assignable to parameter of type '{ id?: string | undefined; checked?: boolean | undefined; label?: string | undefined; labelHidden?: boolean | undefined; indeterminate?: boolean | undefined; disabled?: boolean | undefined; ... 5 more ...; class?: string | undefined; }'.

Generated by 🚫 dangerJS against b7a5d16

@bilal-karim bilal-karim merged commit 039a555 into main Jun 3, 2026
21 of 22 checks passed
@bilal-karim bilal-karim deleted the a11y/1.4.11-checkbox-checked-focus-ring branch June 3, 2026 20:01
rossedfort added a commit that referenced this pull request Jun 4, 2026
Auto-generated version bump from 2.50.0 to 2.51.0

Bump type: minor

Changes included:
- [`495e27f7`](495e27f) Use pageSize instead of maximumPageSize (#3422)
- [`42c58c45`](42c58c4) fix: use rem-based width for expanded side-nav (#3426)
- [`ce928ed5`](ce928ed) fix(a11y): meet 4.5:1 contrast for text-subtle and text-warning (WCAG 1.4.3) (#3437)
- [`e8ed8a49`](e8ed8a4) fix(a11y): consistent high-contrast focus rings across all Button variants (WCAG 1.4.11) (#3438)
- [`03918fb5`](03918fb) fix(a11y): prevent horizontal scroll on login page at 320px (WCAG 1.4.10) (#3440)
- [`a11555fb`](a11555f) fix(a11y): use rem-based font-size and unitless line-height in markdown reset (#3430)
- [`057ff323`](057ff32) fix: use empty alt on decorative SDK logo image (#3424)
- [`e435ffd7`](e435ffd) Standalone Activity details page UI updates (#3427)
- [`e09028f3`](e09028f) Make bottom-nav accept linksSnippet instead of sections (#3445)
- [`a22a1e21`](a22a1e2) Add tooltip for SDK version (#3462)
- [`c3b7820e`](c3b7820) [DT-4039] Workflow query builder doesn't work with numeric search attributes (#3435)
- [`5e53881f`](5e53881) Make Common Errors dismissable (#3471)
- [`2665e2f1`](2665e2f) [DT-4048] Add accessibility PR triage and notification helpers (#3465)
- [`8083cf4e`](8083cf4) feat(DT-4017): Schedules List UI Update (#3467)
- [`b5de30e3`](b5de30e) fix(markdown): allow nested lists to render as block (DT-4047) (#3463)
- [`8a6fd2de`](8a6fd2d) Add a prop to hide select/deselect controls (#3474)
- [`33ec1b3e`](33ec1b3) DT-4051: pre-populate input when starting standalone activity like this one (#3469)
- [`0d64fc20`](0d64fc2) fix(a11y): reveal Copyable's CopyButton on focus-within (WCAG 2.1.1) (#3452)
- [`321651c0`](321651c) fix: accept autocomplete prop on NumberInput, ChipInput, and Combobox (#3425)
- [`6567c222`](6567c22) fix: cap ZoomSvg container height to viewport (#3428)
- [`afd3a8ed`](afd3a8e) fix(a11y): accommodate text-spacing overrides on Badge and Chip (WCAG 1.4.12) (#3433)
- [`d264b876`](d264b87) fix(a11y): non-color signal for Label required indicator (WCAG 1.4.1) (#3439)
- [`73c61ea3`](73c61ea) fix(a11y): tabindex on <main> so the skip link moves focus reliably (WCAG 2.4.1) (#3451)
- [`34e582a8`](34e582a) fix(a11y): info-and-relationships compliance (WCAG 1.3.1) (#3432)
- [`f7be7b6c`](f7be7b6) fix(query): quote ExecutionDuration Go duration strings in visibility SQL (#3482)
- [`9f0c2631`](9f0c263) Passthrough goto params to link component (#3483)
- [`2f8c037b`](2f8c037) feat: add centerButton, menuButton, and linksContent snippets to BottomNavigation (#3485)
- [`7f258bac`](7f258ba) fix: add "for" to validate connection modal title (#3488)
- [`350397ab`](350397a) feat(DT-4069): Update modal backdrop to 50% opaque (#3486)
- [`f01baa49`](f01baa4) refactor: Input & DatePicker - Svelte 5 & afterLabel snippet (#3479)
- [`587c892f`](587c892) Fix decoded object payload summaries (#3491)
- [`2b85ef06`](2b85ef0) fix(DT-4044): only use browser codec endpoint when override option is selected (#3490)
- [`468893ba`](468893b) fix(a11y): improve 1.1.1 non-text content compliance (#3431)
- [`3e8c996d`](3e8c996) fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable (#3429)
- [`47552538`](4755253) fix(a11y): name-role-value compliance, partial (WCAG 4.1.2) (#3434)
- [`e2c95d54`](e2c95d5) fix(a11y): align DOM order with visual order in mobile bottom-nav (WCAG 1.3.2) (#3441)
- [`634b08e1`](634b08e) a11y(1.4.11): focus rings — lighten dark-mode --color-border-focus-info to indigo.400 so ring-primary/70 meets 3:1 against surface-primary (#3478)
- [`039a555a`](039a555) a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background (#3477)
- [`01161e2e`](01161e2) a11y(1.4.10): activity-options drawer — make width responsive so it reflows at 320 px (#3476)
- [`99f0a0a4`](99f0a0a) fix(a11y): filter hidden nav items in mobile bottom-nav (WCAG 4.1.2) (#3494)
- [`d12ebacc`](d12ebac) fix(a11y): add focusin/focusout to saved-query nav hover tooltip (WCAG 2.1.1) (#3453)
- [`c7206af9`](c7206af) fix(a11y): render a warning icon for Chip warning intent (WCAG 1.4.1) (#3450)
- [`b64ed713`](b64ed71) fix(a11y): pair Toast variant background with a severity icon (WCAG 1.4.1) (#3449)
- [`8e2ef708`](8e2ef70) upgrade temporal api version to latest (v1.62.13) (#3502)

Co-authored-by: rossedfort <11775628+rossedfort@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y:bucket-1 Bucket 1: design-mergeable, CSS / tokens a11y:sc-1.4.11 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants